package org.afk.sisy.io;

import java.io.*;
import java.nio.file.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;

import org.afk.sisy.*;
import org.afk.sisy.db.*;
import org.afk.tools.io.*;
import org.afk.tools.log.*;

public class SisyMatcher
{

	final ErrorHandler errorHandler;
	final SisyProgress progress;
	private final SisyJob job;
	private final PathHasher pathHasher;
	private final SisyCompareResultProcessor processor;
	private final PathWrapperMatcher matcher = new PathWrapperMatcher();
	private PathDataBase pathDataBase;

	public SisyMatcher(final SisyJob job, final ErrorHandler errorHandler, final PathHasher pathHasher, final SisyProgress progress,
			final SisyCompareResultProcessor processor)
	{
		this.job = job;
		this.errorHandler = errorHandler;
		this.progress = progress;
		this.pathHasher = pathHasher;
		this.processor = processor;
	}

	private void newSourcePath(final Path path)
	{
		PathWrapper source;
		try
		{
			source = new PathWrapper(this.job.getPrimaryDir(), path);
			if (isRoot(source))
				return;
			if (source.isDirectory())
				this.progress.incSourceFolders(1);
			else
				this.progress.incSourceFiles(1);

			if (!job.isScanSecondaryDir())
				queueNewCompareJob(source, null);
			else
			{
				// use targetScanner
				final PathWrapper target = this.matcher.match(source);
				if (target != null)
					queueNewCompareJob(source, target);
			}
		}
		catch (final IOException e)
		{
			this.errorHandler.warning("While adding " + path, e);
		}
	}

	private void newTargetPath(final Path path)
	{
		PathWrapper target;
		try
		{
			target = new PathWrapper(this.job.getSecondaryDir(), path);
			if (isRoot(target))
				return;

			if (target.isDirectory())
				this.progress.incTargetFolders(1);
			else
				this.progress.incTargetFiles(1);

			final PathWrapper source = this.matcher.match(target);
			if (source != null)
				queueNewCompareJob(source, target);

		}
		catch (final IOException e)
		{
			this.errorHandler.warning("While adding " + path, e);
		}
	}

	private boolean isRoot(final PathWrapper wrapper)
	{
		return wrapper.getAbsolutPath().equals(wrapper.getRoot());
	}

	private void signalDeletedInDb()
	{
		try
		{
			final Collection<PathWrapper> unmatched = this.pathDataBase.selectDeleted(this.job.getSecondaryDir());
			for (final PathWrapper fileWrapper : unmatched)
				queueNewCompareJob(null, fileWrapper);
		}
		catch (final SQLException t)
		{
			this.errorHandler.warning("while requesting deleted files from DB", t);
		}
	}

	private void signalUnmatched()
	{
		final Collection<PathWrapper> unmatched = this.matcher.removeUnmatched();
		for (final PathWrapper fileWrapper : unmatched)
			if (fileWrapper.getRoot().equals(this.job.getPrimaryDir()))
				queueNewCompareJob(fileWrapper, null);
			else
				queueNewCompareJob(null, fileWrapper);

		if (this.job.getDataBase() != null)
			signalDeletedInDb();

	}

	private void startScan(final CountDownLatch countDownLatch, final Side side, final Path root)
	{
		final SisyMatcher me = this;
		new Thread()
		{
			@Override
			public void run()
			{
				try
				{
					SisyMatcher.this.errorHandler.info("Scanning " + side + " " + root);
					Files.walkFileTree(root, new SisyFileVisitor(side, me, SisyMatcher.this.errorHandler));
				}
				catch (final IOException e)
				{
					SisyMatcher.this.errorHandler.warning(e);
				}
				finally
				{
					SisyMatcher.this.errorHandler.info(side + " Scan finished");
					countDownLatch.countDown();
				}
			}
		}.start();
	}

	public void matchFiles()
	{

		try
		{
			final CountDownLatch countDownLatch = new CountDownLatch(2);
			startScan(countDownLatch, Side.PRIMARY, this.job.getPrimaryDir());
			if (!Files.exists(job.getSecondaryDir()))
				Files.createDirectories(job.getSecondaryDir());
			if (job.isScanSecondaryDir())
				startScan(countDownLatch, Side.SECONDARY, this.job.getSecondaryDir());
			else
				countDownLatch.countDown();

			countDownLatch.await();
			this.errorHandler.info("Processing unmatched files");
			signalUnmatched();
		}
		catch (final InterruptedException e)
		{
			this.errorHandler.warning(e);
		}
		catch (final IOException e)
		{
			this.errorHandler.warning(e);
		}
	}

	public void nextFile(final Side key, final Path file)
	{
		if (file.toString().endsWith(SisyConstants.FILE_SUFFIX))
			progress.incSkippedAtticCount(1);
		else if (key == Side.PRIMARY)
			newSourcePath(file);
		else if (key == Side.SECONDARY)
			newTargetPath(file);
	}

	public void queueNewCompareJob(final PathWrapper source, final PathWrapper target)
	{
		try
		{
			this.processor.add(new CompareJob(source, target, this.progress, this.errorHandler, this.job, this.pathHasher, this.pathDataBase));
		}
		catch (final InterruptedException e)
		{
			this.errorHandler.warning(e);
		}
	}

	public void setPathDataBase(final PathDataBase db)
	{
		this.pathDataBase = db;
	}

}
